#Copyright (C) Nial Peters 2012
#
#This file is part of AvoScan.
#
#AvoScan is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.
#
#AvoScan is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with AvoScan.  If not, see <http://www.gnu.org/licenses/>.

"""
The scanning module provides panels to allow the user to input the parameters
needed for setting up scan programs.
"""

import wx
import os
import Queue
import threading

from avoscan import programs, watcher, templates
from avoscan.gui.artwork import AvoscanArtProvider


class ScanningPanel(wx.Panel):
    """
    Panel for holding all the controls associated with creating scan programs.
    """
    def __init__(self, parent, main_frame, scanner):
        self.main_frame = main_frame
        self.scanner = scanner
        self._program_running = False
        wx.Panel.__init__(self, parent)
        vsizer = wx.BoxSizer(wx.VERTICAL)
        self.buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
        
        scan_type_sizer = wx.BoxSizer(wx.HORIZONTAL)
        scan_type_sizer.Add(wx.StaticText(self, -1, "Scan Type:"), 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        
        self.scan_type_choice = wx.Choice(self, wx.ID_ANY, choices=["Continuous", "Stepped"])
        self.scan_type_choice.SetSelection(0)
        scan_type_sizer.Add(self.scan_type_choice, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        vsizer.Add(scan_type_sizer, 0, wx.ALIGN_LEFT | wx.ALL, border=10)
        self.scan_settings_panels = [ContinuousScanSettingsPanel,
                                     SteppedScanSettingsPanel]
        
        self.settings_panel = self.scan_settings_panels[0](self, self.scanner)  
        vsizer.Add(self.settings_panel, 0, wx.EXPAND)
        
        self.logging_panel = LoggingPanel(self)
        vsizer.Add(self.logging_panel, 1, wx.EXPAND)
        
        vsizer.AddSpacer(5)           
        vsizer.Add(self.buttons_sizer, 0, wx.ALIGN_BOTTOM | wx.ALIGN_CENTER_HORIZONTAL)
        vsizer.AddSpacer(5)           
        
        self.start_button = wx.Button(self, wx.ID_ANY, "Start scan")

        self.buttons_sizer.Add(self.start_button, 0, wx.ALIGN_CENTER_HORIZONTAL)
        self.buttons_sizer.AddSpacer(5)
        self.vsizer = vsizer
        self.SetSizer(vsizer)
        vsizer.Fit(self)
        self.SetAutoLayout(1)
          
        wx.EVT_BUTTON(self, self.start_button.GetId(), self.on_start)
        wx.EVT_CHOICE(self, self.scan_type_choice.GetId(), self.on_change_scan_type)
    
    
    def update(self):
        """
        Updates any items on the tab that might have been changed by the user in 
        a different tab. For now that is just the templates list box.
        """
        self.logging_panel.update_templates_list()
    
    
    def on_change_scan_type(self, evnt):
        """
        Event handler for handling changes in the scan type selection. Displays
        the relevant scan settings panel for the selected scan type.
        """
        new_scan_settings = self.scan_settings_panels[self.scan_type_choice.GetSelection()](self, self.scanner)

        self.vsizer.Replace(self.settings_panel, new_scan_settings)
        self.settings_panel.Destroy()
        self.settings_panel = new_scan_settings
        self.vsizer.Fit(self)
        self.vsizer.Layout()

    
    def on_start(self, event):
        """
        Event handler for handling 'Start Scan' button clicks. Reads all
        the settings from the controls in the ScanningPanel, creates an 
        appropriate ScanProgram, displays a ScanningProgressFrame and 
        starts the scan running.
        """
        #TODO - this is a quick hack for now, this needs tidying up
        try:
            wx.BeginBusyCursor()
            extra_procs = []
            if self.logging_panel.is_dir_watching_enabled():
                dir_ = self.logging_panel.get_dir_to_watch()
                spec_time_recorder = watcher.create_dir_watcher(dir_)
                
                if spec_time_recorder is not None:
                    extra_procs.append(spec_time_recorder)
                    
            if self.scan_type_choice.GetSelection() == 1:
                try:
                    start_angle = self.settings_panel.get_start_angle()
                    end_angle = self.settings_panel.get_end_angle()
                    num_steps = self.settings_panel.get_num_steps()
                    step_time = self.settings_panel.get_step_time()
                
                except InvalidValue:
                    return
                
                if self.logging_panel.is_logging_enabled():
                    try:
                        log_file = self.logging_panel.get_filename()
                        field_notes = self.logging_panel.get_field_notes()
                        prog = programs.SteppedScanProgramLogged(self.scanner, start_angle, end_angle, num_steps, step_time, log_file, field_notes, extra_procs=extra_procs)
              
                        if os.path.exists(log_file):
                            m = wx.MessageDialog(self, log_file + " exists. Overwrite?", "AvoScan", wx.ICON_WARNING | wx.YES_NO)
                            if m.ShowModal() == wx.ID_NO:
                                return
                        
                        self._program_running = True
                        prog.start()    
                        ScanningProgressFrame(self.main_frame, self.scanner, prog)
                        self.main_frame.Show(False)
                        return
                                        
                    except InvalidValue:
                        return
                else:
                    wx.MessageBox("You must enable logging to use this scan type", "AvoScan", wx.ICON_ERROR)
                    return
    
            
            #verify the user supplied values
            try:
                start_angle = self.settings_panel.get_start_angle()
                end_angle = self.settings_panel.get_end_angle()
                scan_time = self.settings_panel.get_scan_time(start_angle, end_angle)
            
            except InvalidValue:
                return
            
            #generate a new scan program
            
            
                   
            if self.logging_panel.is_logging_enabled():
                try:
                    log_file = self.logging_panel.get_filename()
                    field_notes = self.logging_panel.get_field_notes()
                    prog = programs.ScanProgramLogged(self.scanner, start_angle, end_angle, scan_time, log_file, field_notes, extra_procs=extra_procs)
          
                    if os.path.exists(log_file):
                        m = wx.MessageDialog(self, log_file + " exists. Overwrite?", "AvoScan", wx.ICON_WARNING | wx.YES_NO)
                        if m.ShowModal() == wx.ID_NO:
                            return
                        
                except InvalidValue:
                    return
            else:
                prog = programs.ScanProgramSimple(self.scanner, start_angle, end_angle, scan_time, extra_procs=extra_procs)
            self._program_running = True
              
            ScanningProgressFrame(self.main_frame, self.scanner, prog)
            prog.start()  
            self.main_frame.Show(False)
            
        finally:
            wx.EndBusyCursor()
            wx.Yield()
        

class EditScanParametersDialog(wx.Dialog):
    """
    Dialog for changing the scan angles and scan speed during a scan.
    """
    def __init__(self, parent, scanner, scan_program):
        self.scanner = scanner
        self.scan_program = scan_program
        wx.Dialog.__init__(self, parent, wx.ID_ANY, "AvoScan - Edit scan parameters")
        self.scan_settings_panel = ContinuousScanSettingsPanel(self, scanner)
        
        #set the values in the settings boxes to the current scan values
        self.scan_settings_panel.start_angle_box.SetValue(str(scan_program.start_angle))
        self.scan_settings_panel.end_angle_box.SetValue(str(scan_program.end_angle))
        deg_per_sec = abs(scan_program.end_angle - scan_program.start_angle) / (scan_program.duration * 60.0)
        
        self.scan_settings_panel.scan_time_box.SetValue(str(deg_per_sec))
               
        vsizer = wx.BoxSizer(wx.VERTICAL)
        buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
        
        vsizer.Add(self.scan_settings_panel, 1, wx.EXPAND)
        
        self.set_button = wx.Button(self, wx.ID_ANY, "Set")
        self.cancel_button = wx.Button(self, wx.ID_ANY, "Cancel")
        buttons_sizer.Add(self.set_button, 0, wx.ALIGN_RIGHT)
        buttons_sizer.Add(self.cancel_button, 0, wx.ALIGN_RIGHT)

        vsizer.Add(buttons_sizer, 0, wx.ALIGN_RIGHT | wx.ALIGN_BOTTOM | wx.ALL, border=10)
    
        wx.EVT_BUTTON(self, self.cancel_button.GetId(), self.onCancel)
        wx.EVT_BUTTON(self, self.set_button.GetId(), self.onSet)
        
        self.SetSizer(vsizer)
        self.SetAutoLayout(1)
        vsizer.Fit(self)
    
    
    def onCancel(self, evnt):
        """
        Event handler for handling Cancel button clicks. Discards any changes and 
        returns to the ScanningProgressFrame.
        """
        self.EndModal(wx.ID_CANCEL)
    
    
    def onSet(self, evnt):
        """
        Event handler for handling 'Set' button clicks. Reads the new scan parameters
        from the controls, sets them on the scanner and returns to the ScanningProgressFrame.
        """
        try:
            new_start_angle = self.scan_settings_panel.get_start_angle()
            new_end_angle = self.scan_settings_panel.get_end_angle()
            new_duration = self.scan_settings_panel.get_scan_time(new_start_angle, new_end_angle)
        except InvalidValue:
            return
        
        self.scan_program.set_start_angle(new_start_angle)
        self.scan_program.set_end_angle(new_end_angle)
        self.scan_program.set_duration(new_duration)
        
        self.EndModal(wx.ID_OK)
        


class ScanningProgressFrame(wx.Frame):
    """
    Frame to display the current position of the scanner while it is scanning.
    A 'Stop' button gives the user the option to abort the scan.
    """
    def __init__(self, main_frame, scanner, scan_program):
        self.main_frame = main_frame
        self.scanner = scanner
        self.scan_prog = scan_program
        self.exit_queue = Queue.Queue()
        self.update_thread = threading.Thread(target=self.__update_position)
        self.edit_dialog = None
        wx.Frame.__init__(self, None, wx.ID_ANY, "AvoScan - Scanning...", style=wx.MINIMIZE_BOX | wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.CLIP_CHILDREN)
        
        top_panel = wx.Panel(self, wx.ID_ANY)

        art = AvoscanArtProvider()
        wx.ArtProvider.Push(art)
        icon = wx.StaticBitmap(top_panel, bitmap=art.GetBitmap("Avoscan", client=wx.ART_MESSAGE_BOX))
        self.SetIcon(art.GetIcon("Avoscan"))
        
        self.vsizer = wx.BoxSizer(wx.VERTICAL)
        self.hsizer = wx.BoxSizer(wx.HORIZONTAL)
        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
        
        self.vsizer.AddSpacer(10)
        self.vsizer.Add(self.hsizer, 1, wx.EXPAND | wx.ALIGN_CENTER)
        self.hsizer.AddSpacer(20)
        self.hsizer.Add(icon, 0, wx.ALIGN_CENTER)
        self.hsizer.AddSpacer(20)
        self.status_text = wx.StaticText(top_panel, wx.ID_ANY, "Scanning.....         ")
        self.hsizer.Add(self.status_text, 0, wx.ALIGN_CENTER)
        self.hsizer.AddSpacer(20)
        
        self.stop_button = wx.Button(top_panel, wx.ID_ANY, 'Stop')
        
        if isinstance(self.scan_prog, (programs.ScanProgramLogged, programs.ScanProgramSimple)):
            self.edit_button = wx.Button(top_panel, wx.ID_ANY, 'Edit')        
            self.button_sizer.Add(self.edit_button, 0, wx.ALIGN_RIGHT)
            wx.EVT_BUTTON(self, self.edit_button.GetId(), self.on_edit)
            
        self.button_sizer.Add(self.stop_button, 0, wx.ALIGN_RIGHT)    
        self.vsizer.Add(self.button_sizer, 0, wx.ALIGN_RIGHT | wx.ALIGN_BOTTOM | wx.ALL, border=10)
        
        wx.EVT_CLOSE(self, self.on_stop)
        wx.EVT_BUTTON(self, self.stop_button.GetId(), self.on_stop)
        
        self.scanner.set_detach_handler(self.onScannerDisconnect)
        
        top_panel.SetSizer(self.vsizer)
        
        top_panel.SetAutoLayout(1)
        self.vsizer.Fit(self)
        self.CenterOnScreen()
        self.Show()
        self.update_thread.start()
        

    def __update_position(self):
        """
        Updates the status text to show the current position of the scanner. Can be
        called from any thread.
        """       
        while True:
            pos = self.scanner.get_position()
            wx.MutexGuiEnter()
            self.status_text.SetLabel("Scanning....." + str(round(pos, 1)) + u"\u00b0")
            wx.MutexGuiLeave()
            try:
                self.exit_queue.get(timeout=1)
                break
            except Queue.Empty:
                pass

    
    def onScannerDisconnect(self, evnt):
        """
        Event handler for scanner disconnect events. Shows a warning message and 
        exits the program.
        """
        self.exit_queue.put(None)
        self.update_thread.join()
        self.scan_prog.stop()
        if self.edit_dialog is not None:
            if self.edit_dialog.IsModal():
                self.edit_dialog.Close()
        wx.Yield()
        wx.CallAfter(wx.MessageBox, "Scanner disconnected! Check the connection to your computer.", "AvoScan", wx.ICON_ERROR)
        
        wx.CallAfter(self.main_frame.Destroy)
        self.Destroy()
    
    
    def on_edit(self, evnt):
        """
        Event handler for 'Edit' button clicks. Opens an EditScanParametersDialog 
        to allow changing the scan angles and scan speed.
        """
        self.edit_dialog = EditScanParametersDialog(self, self.scanner, self.scan_prog)
        wx.CallAfter(self.edit_dialog.ShowModal)
        
         
    def on_stop(self, evnt):       
        """
        Event handler for 'Stop' button clicks. Stops the scan program, returns
        the scanner to its zenith position and returns to the main AvoScan frame.
        """ 
        self.exit_queue.put(None)
        self.update_thread.join()
        wx.BeginBusyCursor()
        self.stop_button.Disable()
        self.status_text.SetLabel("Stopping...")
        self.scan_prog.stop()

        #return the scanner to the zenith position
        self.scanner.set_to_tracking_speed()
        self.scanner.set_position(90)
        self.scanner.disengage()
        self.Show(False)
        wx.EndBusyCursor()

        #return to the main window
        self.main_frame.Show(True)
        
        self.Destroy()        
        

class ContinuousScanSettingsPanel(wx.Panel):
    """
    Panel to hold controls for inputting the settings needed for continuous scanning.
    """
    def __init__(self, parent, scanner):
        wx.Panel.__init__(self, parent, wx.ID_ANY, style=wx.BORDER_SUNKEN | wx.TAB_TRAVERSAL)
        self.scanner = scanner
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer1 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer2 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer3 = wx.BoxSizer(wx.HORIZONTAL)   
        
        vsizer.AddSpacer(10)
        vsizer.Add(hsizer1, 0, wx.ALIGN_LEFT | wx.EXPAND)
        vsizer.AddSpacer(5)
        vsizer.Add(hsizer2, 0, wx.ALIGN_LEFT | wx.EXPAND)
        vsizer.AddSpacer(5)
        vsizer.Add(hsizer3, 0, wx.ALIGN_LEFT | wx.EXPAND)
        vsizer.AddSpacer(10)
        
        self.start_angle_box = wx.TextCtrl(self, wx.ID_ANY)
        self.end_angle_box = wx.TextCtrl(self, wx.ID_ANY)
        self.scan_time_box = wx.TextCtrl(self, wx.ID_ANY)
        self.scan_time_units_choice = wx.Choice(self, wx.ID_ANY, choices=["degrees per sec.", "minutes"])
        self.scan_time_units_choice.SetSelection(0)
        
        hsizer1.AddSpacer((20, 0))
        start_angle_text = wx.StaticText(self, wx.ID_ANY, u"Start angle (0-180\u00b0) :")
        hsizer1.Add(start_angle_text, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)        
        hsizer1.Add(self.start_angle_box, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        hsizer1.AddSpacer((20, 0))
        hsizer2.AddSpacer((20, 0))
        
        hsizer2.Add(wx.StaticText(self, wx.ID_ANY, u"End angle (0-180\u00b0) : ", size=start_angle_text.GetBestSizeTuple()), 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        hsizer2.Add(self.end_angle_box, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        hsizer2.AddSpacer((20, 0))
        hsizer3.AddSpacer((20, 0))
        
        hsizer3.Add(wx.StaticText(self, wx.ID_ANY, "Scan speed : ", size=start_angle_text.GetBestSizeTuple()), 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        hsizer3.Add(self.scan_time_box, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        hsizer3.AddSpacer(5)
        hsizer3.Add(self.scan_time_units_choice, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        hsizer3.AddSpacer((20, 0))
        self.SetSizer(vsizer)
        vsizer.Fit(self)
        self.SetAutoLayout(1)
        
        self.start_angle_box.SetFocus()


    def invalid_user_input(self, message):
        """
        Display 'message' in a wx.MessageBox with an error icon.
        """
        wx.MessageBox(message, "AvoScan", wx.ICON_ERROR)


    def get_start_angle(self):
        """
        Validates and returns the value in the start angle text box. If the 
        value is invalid, then displays an error message and raises InvalidValue
        """
        txt = self.start_angle_box.GetValue()
        if txt.isspace() or txt == "":
            self.invalid_user_input("No start angle specified.")
            raise InvalidValue
        try:
            val = float(txt)
        except:
            self.invalid_user_input("Start angle must be a number of degrees")
            raise InvalidValue
        
        if val < 0 or val > 180:
            self.invalid_user_input("Start angle must be in the range 0-180 degrees.")
            raise InvalidValue
        
        return val
    
    
    def get_end_angle(self):
        """
        Validates and returns the value in the end angle text box. If the 
        value is invalid, then displays an error message and raises InvalidValue
        """
        txt = self.end_angle_box.GetValue()
        if txt.isspace() or txt == "":
            self.invalid_user_input("No end angle specified.")
            raise InvalidValue
        try:
            val = float(txt)
        except:
            self.invalid_user_input("End angle must be a number of degrees")
            raise InvalidValue
        
        if val < 0 or val > 180:
            self.invalid_user_input("End angle must be in the range 0-180 degrees.")
            raise InvalidValue
        
        return val
    
    
    def get_scan_time(self, start_angle, end_angle):
        """
        Validates and returns the value in the scan speed text box. If the 
        value is invalid, then displays an error message and raises InvalidValue
        """
        txt = self.scan_time_box.GetValue()
        if txt.isspace() or txt == "":
            self.invalid_user_input("No scan speed specified.")
            raise InvalidValue
        try:
            val = float(txt)
        except:
            self.invalid_user_input("Scan speed must be a number")
            raise InvalidValue
        
        if self.scan_time_units_choice.GetSelection() == 0:
            #then it's in degrees per second
            velocity = val
            scan_time = (abs(end_angle - start_angle) / velocity) / 60.0
        else:
            #the it's set to minutes            
            scan_time = val
            velocity = abs(end_angle - start_angle) / (scan_time * 60.0)
        
        #check that the scan speeds are sane
        if self.scanner.degrees2steps(velocity) < 10:
            #8 is actually the physical limit for the motor
            self.invalid_user_input("Cannot scan this slowly!")
            raise InvalidValue
        if velocity >= self.scanner.get_tracking_speed():
            self.invalid_user_input("Cannot scan this fast!")
            raise InvalidValue

        return scan_time

        
class InvalidValue(ValueError):
    """
    Exception raised if a user input value is not valid.
    """
    pass


class LoggingPanel(wx.Panel):
    """
    Panel to hold all the controls for inputting the settings for logging scan angles
    to a file.
    """
    def __init__(self, parent):
        self._enable_logging = False
        self._enable_dir_watching = False
        self._current_notes_template = "None"
        self.__dir = "" #working dir to set dir watch dialog to
        self.template_manager = templates.TemplateManager()
        
        wx.Panel.__init__(self, parent, wx.ID_ANY, style=wx.BORDER_SUNKEN | wx.TAB_TRAVERSAL)
        
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer1 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer3 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer4 = wx.BoxSizer(wx.HORIZONTAL)
        
        self.logging_check_box = wx.CheckBox(self, wx.ID_ANY, "Log to file")
        self.dir_watching_check_box = wx.CheckBox(self, wx.ID_ANY, "Record spectra times")
        hsizer1.AddSpacer((20, 0))
        hsizer1.Add(self.logging_check_box, 0, wx.ALIGN_LEFT)
        hsizer1.AddSpacer((20, 0))
        hsizer1.Add(self.dir_watching_check_box, 0, wx.ALIGN_LEFT)
        
        self.dir_watching_check_box.Disable()
        
        self.browse_button = wx.Button(self, wx.ID_ANY, "Browse")
        #make filename box the same height as the button
        self.filename_box = wx.TextCtrl(self, wx.ID_ANY, size=(250, self.browse_button.GetSize()[1]))
        self.filename_box.Disable()
        self.browse_button.Disable()
        hsizer3.AddSpacer((20, 0))
        hsizer3.Add(self.filename_box, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL)
        hsizer3.Add(self.browse_button, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL)
        
        self.browse_button2 = wx.Button(self, wx.ID_ANY, "Browse")
        #make filename box the same height as the button
        self.spec_dir_box = wx.TextCtrl(self, wx.ID_ANY, size=(250, self.browse_button2.GetSize()[1]))
        self.spec_dir_box.Disable()
        self.browse_button2.Disable()
        hsizer4.AddSpacer((20, 0))
        hsizer4.Add(self.spec_dir_box, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL)
        hsizer4.Add(self.browse_button2, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL)
        
        vsizer.Add(hsizer1, 0, wx.TOP | wx.BOTTOM, border=10)

        vsizer.Add(wx.StaticText(self, wx.ID_ANY, "Filename:"), 0, wx.ALIGN_LEFT | wx.LEFT, border=20)
        vsizer.Add(hsizer3)
        vsizer.AddSpacer(10)
        
        vsizer.Add(wx.StaticText(self, wx.ID_ANY, "Spectra output directory:"), 0, wx.ALIGN_LEFT | wx.LEFT, border=20)
        vsizer.Add(hsizer4)
        vsizer.AddSpacer(10)
                
        #add the field notes box and template selection menu
        notes_title_sizer = wx.BoxSizer(wx.HORIZONTAL)
        notes_title_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Field Notes:"), 0, wx.ALIGN_LEFT | wx.ALIGN_BOTTOM)
        notes_title_sizer.AddStretchSpacer()
        notes_title_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Notes Template:"), 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        
        #set up the choice box for notes templates
        self.notes_template_choice = wx.Choice(self, wx.ID_ANY, choices=[])
        self.update_templates_list()
        
        notes_title_sizer.Add(self.notes_template_choice, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        vsizer.Add(notes_title_sizer, 0, wx.ALIGN_LEFT | wx.LEFT | wx.RIGHT | wx.EXPAND, border=10)
        self.field_notes_box = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_MULTILINE | wx.TE_RICH)
        
        vsizer.Add(self.field_notes_box, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=10)
        self.field_notes_box.Disable()
        self.field_notes_box.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))

        self.SetSizer(vsizer)
        vsizer.Fit(self)
        self.SetAutoLayout(1)
        
        wx.EVT_CHECKBOX(self, self.logging_check_box.GetId(), self.on_logging_checkbox)
        wx.EVT_CHECKBOX(self, self.dir_watching_check_box.GetId(), self.on_watchdirs_checkbox)
        wx.EVT_BUTTON(self, self.browse_button.GetId(), self.on_browse)
        wx.EVT_BUTTON(self, self.browse_button2.GetId(), self.on_watchdir_browse)
        wx.EVT_CHOICE(self, self.notes_template_choice.GetId(), self.on_template_choice)
        
        #default is to output to file and to time spectra creation if possible
        self.logging_check_box.SetValue(True)
        self.on_logging_checkbox(None)
        if watcher.can_watch_directories():
            self.dir_watching_check_box.Enable()
            self.dir_watching_check_box.SetValue(True)
            self.on_watchdirs_checkbox(None)
    
    
    def update_templates_list(self):
        self.notes_templates_list = ["None"] + self.template_manager.get_template_names()
        self.notes_template_choice.SetItems(self.notes_templates_list)
        
        #set the selection back to one we were last using
        try:
            idx = self.notes_templates_list.index(self._current_notes_template)
            self.notes_template_choice.SetSelection(idx)
        except ValueError:
            #if the template we are currently using has now been deleted - then just set the 
            #selection to None. This isn't ideal, but really how often is this going to happen?
            self.notes_template_choice.SetSelection(0)

    
    def on_template_choice(self, evnt):
        """
        Event handler for dealing with choices of field notes templates. Loads the 
        requested template and fills in the fieldnotes box.
        """
        selected_template = self.notes_templates_list[self.notes_template_choice.GetCurrentSelection()]
        if selected_template == self._current_notes_template:
            return
                
        #check that we are not going to overwrite fieldnotes that have already been 
        #written.
        current_notes = self.field_notes_box.GetValue()
        
        if current_notes != self.template_manager.load_template(self._current_notes_template):
            mb = wx.MessageDialog(self, "This will overwrite your current fieldnotes!")
            if mb.ShowModal() == wx.ID_CANCEL:
                self.notes_template_choice.SetSelection(self.notes_templates_list.index(self._current_notes_template))
                return
        

        self.field_notes_box.SetValue(self.template_manager.load_template(selected_template))
        self._current_notes_template = selected_template
        
          
    def get_filename(self):
        """
        Validates and returns the filename for the file to log the angles to. If the filename
        is invalid or unwritable then displays an error message and raises InvalidValue. If 
        logging is not enabled, then this will return None.
        """
        if not self._enable_logging:
            return None
        
        txt = self.filename_box.GetValue().lstrip().rstrip()
        if txt.isspace() or txt == "":
            self.invalid_user_input("No filename specified for logging.")
            raise InvalidValue 
        
        if not os.path.isdir(os.path.dirname(txt)):
            self.invalid_user_input("Cannot create angles file. No such directory: \'" + os.path.dirname(txt) + "\'")
            raise InvalidValue
        
        if os.path.exists(txt):
            #check if it is writeable (could be a file that we don't have permission to edit)
            if not os.access(txt, os.W_OK):
                self.invalid_user_input("File \'" + txt + "\' already exists. Cannot overwrite. Permission denied.")
                raise InvalidValue
            
            #check if the filename already exists as a directory (in which case we 
            #won't be able to open it for writing - despite what os.access says).
            if os.path.isdir(txt):
                self.invalid_user_input("Cannot create angles file. \'" + txt + "\' is a directory.")
                raise InvalidValue
               
        return txt
 
    
    def get_dir_to_watch(self):
        """
        Validates and returns the path where the spectra will be stored. If the path
        is invalid then displays an error message and raises InvalidValue. If directory
        watching is not enabled, then returns None.
        """
        if not self._enable_dir_watching:
            return None
        
        txt = self.spec_dir_box.GetValue()
        if txt.isspace() or txt == "":
            self.invalid_user_input("No spectra output directory specified.")
            raise InvalidValue
        
        if not os.path.isdir(txt):
            self.invalid_user_input("No such directory: \'" + txt + "\'")
            raise InvalidValue
         
        return txt.lstrip().rstrip()
    
    
    def get_field_notes(self):
        """
        Returns a list of lines of text from the field notes box.
        """
        txt = self.field_notes_box.GetValue()
        
        if txt.isspace() or txt == "":
            return []
        
        return txt.split('\n')
    
    
    def on_watchdirs_checkbox(self, evnt):
        """
        Event handler for directory watching checkbox events. Enables/Disables
        the directory watching controls.
        """
        if self.dir_watching_check_box.IsChecked():
            self.spec_dir_box.Enable()
            self.browse_button2.Enable()
            self._enable_dir_watching = True
        else:
            self.spec_dir_box.Disable()
            self.browse_button2.Disable()
            self._enable_dir_watching = False

    
    def on_logging_checkbox(self, evnt):
        """
        Event handler for angles logging checkbox events. Enables/Disables
        the logging controls.
        """
        if self.logging_check_box.IsChecked():
            self.filename_box.Enable()
            self.browse_button.Enable()
            self.field_notes_box.Enable()
            self.field_notes_box.SetForegroundColour(wx.BLACK)
            self._enable_logging = True
        
        else:
            self.filename_box.Disable()
            self._enable_logging = False
            self.browse_button.Disable()
            self.field_notes_box.Disable()
            self.field_notes_box.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))

            
    def invalid_user_input(self, message):
        """
        Display 'message' in a wx.MessageBox with an error icon.
        """
        wx.MessageBox(message, "AvoScan", wx.ICON_ERROR)


    def on_watchdir_browse(self, evnt):
        """
        Event handler for browse button. Opens a directory choice dialog.
        """
        dialog = wx.DirDialog(self, "Choose spectra output directory", defaultPath=self.__dir)
        if dialog.ShowModal() != wx.ID_CANCEL:
            self.spec_dir_box.SetValue(dialog.GetPath())
            self.__dir = dialog.GetPath()

    
    def on_browse(self, evnt):
        """
        Event handler for browse button. Opens a file save dialog.
        """
        dialog = wx.FileDialog(self, "Choose output file", style=wx.FD_SAVE, defaultDir=self.__dir)
        
        if dialog.ShowModal() != wx.ID_CANCEL:
            self.filename_box.SetValue(dialog.GetPath())
            self.__dir = dialog.GetPath()


    def is_dir_watching_enabled(self):
        """
        Returns True if directory watching is enabled. False otherwise.
        """
        return self._enable_dir_watching
  
    
    def is_logging_enabled(self):
        """
        Returns True if logging is enabled. False otherwise.
        """
        return self._enable_logging



class SteppedScanSettingsPanel(wx.Panel):
    """
    Panel to hold controls for inputting the settings needed for stepped scanning.
    """
    def __init__(self, parent, scanner):
        wx.Panel.__init__(self, parent, wx.ID_ANY, style=wx.BORDER_SUNKEN | wx.TAB_TRAVERSAL)
        self.scanner = scanner
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer1 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer2 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer3 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer4 = wx.BoxSizer(wx.HORIZONTAL)
        
        vsizer.AddSpacer(10)
        vsizer.Add(hsizer1, 0, wx.ALIGN_LEFT | wx.EXPAND)
        vsizer.AddSpacer(5)
        vsizer.Add(hsizer2, 0, wx.ALIGN_LEFT | wx.EXPAND)
        vsizer.AddSpacer(5)
        vsizer.Add(hsizer3, 0, wx.ALIGN_LEFT | wx.EXPAND)
        vsizer.AddSpacer(5)
        vsizer.Add(hsizer4, 0, wx.ALIGN_LEFT | wx.EXPAND)
        vsizer.AddSpacer(10)
        
        self.start_angle_box = wx.TextCtrl(self, wx.ID_ANY)
        self.end_angle_box = wx.TextCtrl(self, wx.ID_ANY)
        self.num_steps_box = wx.TextCtrl(self, wx.ID_ANY)
        self.pause_time_box = wx.TextCtrl(self, wx.ID_ANY)
        
        hsizer1.AddSpacer((20, 0))
        start_angle_text = wx.StaticText(self, wx.ID_ANY, u"Start angle (0-180\u00b0) :")
        hsizer1.Add(start_angle_text, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)        
        hsizer1.Add(self.start_angle_box, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        hsizer1.AddSpacer((20, 0))
        
        hsizer2.AddSpacer((20, 0))        
        hsizer2.Add(wx.StaticText(self, wx.ID_ANY, u"End angle (0-180\u00b0) : ", size=start_angle_text.GetBestSizeTuple()), 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        hsizer2.Add(self.end_angle_box, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        hsizer2.AddSpacer((20, 0))
        
        hsizer3.AddSpacer((20, 0))        
        hsizer3.Add(wx.StaticText(self, wx.ID_ANY, u"Number of steps : ", size=start_angle_text.GetBestSizeTuple()), 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        hsizer3.Add(self.num_steps_box, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        hsizer3.AddSpacer((20, 0))    
        
        hsizer4.AddSpacer((20, 0))        
        hsizer4.Add(wx.StaticText(self, wx.ID_ANY, u"Seconds per step : ", size=start_angle_text.GetBestSizeTuple()), 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        hsizer4.Add(self.pause_time_box, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL)
        hsizer4.AddSpacer((20, 0))    
        
        self.SetSizer(vsizer)
        vsizer.Fit(self)
        self.SetAutoLayout(1)
        

    def invalid_user_input(self, message):
        """
        Display 'message' in a wx.MessageBox with an error icon.
        """
        wx.MessageBox(message, "AvoScan", wx.ICON_ERROR)


    def get_start_angle(self):
        """
        Validates and returns the value in the start angle text box. If the 
        value is invalid, then displays an error message and raises InvalidValue
        """
        txt = self.start_angle_box.GetValue()
        if txt.isspace() or txt == "":
            self.invalid_user_input("No start angle specified.")
            raise InvalidValue
        try:
            val = float(txt)
        except:
            self.invalid_user_input("Start angle must be a number of degrees")
            raise InvalidValue
        
        if val < 0 or val > 180:
            self.invalid_user_input("Start angle must be in the range 0-180 degrees.")
            raise InvalidValue
        
        return val
    
    
    def get_end_angle(self):
        """
        Validates and returns the value in the end angle text box. If the 
        value is invalid, then displays an error message and raises InvalidValue
        """
        txt = self.end_angle_box.GetValue()
        if txt.isspace() or txt == "":
            self.invalid_user_input("No end angle specified.")
            raise InvalidValue
        try:
            val = float(txt)
        except:
            self.invalid_user_input("End angle must be a number of degrees.")
            raise InvalidValue
        
        if val < 0 or val > 180:
            self.invalid_user_input("End angle must be in the range 0-180 degrees.")
            raise InvalidValue
        
        return val
    
    
    def get_num_steps(self):
        """
        Validates and returns the value in the number of steps text box. If the 
        value is invalid, then displays an error message and raises InvalidValue
        """
        txt = self.num_steps_box.GetValue()
        if txt.isspace() or txt == "":
            self.invalid_user_input("Number of steps not specified.")
            raise InvalidValue
        try:
            val = int(txt)
        except:
            self.invalid_user_input("Number of steps must be an integer.")
            raise InvalidValue
        
        if val < 2:
            self.invalid_user_input("Number of steps must be larger than 2.")
            raise InvalidValue 

        return val


    def get_step_time(self):
        """
        Validates and returns the value in the step time text box. If the 
        value is invalid, then displays an error message and raises InvalidValue
        """
        txt = self.pause_time_box.GetValue()
        if txt.isspace() or txt == "":
            self.invalid_user_input("Step time is not specified.")
            raise InvalidValue
        try:
            val = float(txt)
        except:
            self.invalid_user_input("Step time must be a number (number of seconds).")
            raise InvalidValue
        
        if val < 0:
            self.invalid_user_input("Step time must be greater than or equal to 0.")
            raise InvalidValue 

        return val
